package com.stark.lock.aspect;

import com.stark.lock.annotation.DistributedLock;
import com.stark.lock.aop.MethodExpressionEvaluator;
import com.stark.lock.exception.DistributedLockException;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.integration.support.locks.LockRegistry;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * 分布式锁切面切面。
 * <p>作用于所有 {@link DistributedLock @DistributedLock} 注解的方法。
 *
 * @author <a href="mailto:mengbin@hotmail.com">Ben</a>
 * @version 1.0.0
 * @since 1.0.0
 */
@Aspect
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class DistributedLockAspect implements Ordered {

    private LockRegistry lockRegistry;

    private long timeout;

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Around("@annotation(com.stark.lock.annotation.DistributedLock)")
    public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        DistributedLock lockConfig = method.getAnnotation(DistributedLock.class);
        MethodExpressionEvaluator evaluator = new MethodExpressionEvaluator(joinPoint);
        String lockKey = String.valueOf(evaluator.getValue(lockConfig.key()));
        String target = evaluator.getTargetClass().getName() + "#" + evaluator.getTargetMethod().getName();
        long _timeout = lockConfig.timeout() > 0
                ? lockConfig.timeout()
                : timeout;

        String id = UUID.randomUUID().toString().replace("-", "");
        log.info("\n-->" + timestamp() + " --- [DistributedLockAspect] 尝试占锁: id=" + id
                + "\n   target=" + target
                + "\n   lockKey=" + lockKey
                + "\n   timeout=" + _timeout);
        Lock lock = lockRegistry.obtain(lockKey);
        boolean isLocked;
        try {
            isLocked = _timeout > 0
                    ? lock.tryLock(_timeout, TimeUnit.MILLISECONDS)
                    : lock.tryLock();
        } catch (Exception e) {
            log.error("\n-->" + timestamp() + " --- [DistributedLockAspect] 占锁失败: id=" + id
                    + "\n   target=" + target
                    + "\n   lockKey=" + lockKey
                    + "\n   timeout=" + _timeout, e);
            throw new DistributedLockException("占锁失败: target=" + target + ",lockKey=" + lockKey + ",timeout=" + _timeout, e);
        }
        if (!isLocked) {
            log.info("\n-->" + timestamp() + " --- [DistributedLockAspect] 占锁失败: id=" + id
                    + "\n   target=" + target
                    + "\n   lockKey=" + lockKey
                    + "\n   timeout=" + _timeout);
            throw new DistributedLockException("占锁失败: target=" + target + ",lockKey=" + lockKey + ",timeout=" + _timeout);
        }

        log.info("\n-->" + timestamp() + " --- [DistributedLockAspect] 占锁成功: id=" + id
                + "\n   target=" + target
                + "\n   lockKey=" + lockKey);
        try {
            return joinPoint.proceed();
        } finally {
            try {
                lock.unlock();
            } catch (Exception e) {
                // Lock was released in the store due to expiration, do nothing.
            }
            log.info("\n-->" + timestamp() + " --- [DistributedLockAspect] 解锁成功: id=" + id
                    + "\n   target=" + target
                    + "\n   lockKey=" + lockKey
                    + "\n   timeout=" + _timeout);
        }
    }

    private String timestamp() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
    }

}
